// SPDX-FileCopyrightText: 2017-2023 Giovanni Dante Grazioli <deroad@libero.it>
// SPDX-FileCopyrightText: 2018-2019 elicn
// SPDX-License-Identifier: BSD-3-Clause

import Cpp from '../db/cpp.js';
import CCalls from '../db/c_calls.js';
import Extra from './extra.js';
import Variable from './variable.js';
import Condition from './condition.js';
import Long from '../long.js';

/**
 * Wraps a string with parenthesis.
 * @param {string} s A string to wrap
 * @returns {string} `s` wrapped by parenthesis
 */
var parenthesize = function(s) {
	return ['(', s, ')'].join('');
};

/**
 * Wraps a string with parenthesis only if it is complex.
 * @param {string} s A string to wrap
 * @returns {string} `s` wrapped by parenthesis if `s` is a complex string, and `s` otherwise
 */
var autoParen = function(s) {
	return (s.indexOf(' ') > (-1) ? parenthesize(s) : s);
};

var autoString = function(v) {
	v = v || "";
	return Extra.is.string(v) ? Global().printer.auto(v) : v.toString();
};

var _generic_asm = function(asm) {
	this.asm = asm;

	this.toString = function() {
		return Global().printer.theme.callname('__asm') + ' (' + Global().printer.theme.text('"' + this.asm + '"') + ')';
	};
};

/**
 * Unary expression
 * @constructor
 * @inner
 */
var _uexpr = function(operator, operand) {
	this.operator = operator;

	this.operands = [
		autoString(operand)
	];

	/** @returns {!string} */
	this.toString = function() {
		return [
			this.operator,
			this.operands[0]
		].join('');
	};
};

/**
 * Unary expression with postfix notation
 * @constructor
 * @inner
 */
var _uexpr_pf = function(operator, operand) {
	_uexpr.call(this, operator, [operand]);

	/** @returns {!string} */
	this.toString = function() {
		return [
			this.operands[0],
			this.operator,
		].join('');
	};
};

_uexpr_pf.prototype = Object.create(_uexpr.prototype);

/**
 * Binary expression
 * @constructor
 * @inner
 */
var _bexpr = function(operator, lhand, rhand) {
	this.operator = operator;

	this.operands = [
		autoString(lhand),
		autoString(rhand)
	];

	/** @returns {!string} */
	this.toString = function() {
		return [
			this.operands[0],
			this.operator,
			this.operands[1]
		].join(' ');
	};
};

/**
 * Ternary expression
 * @constructor
 * @inner
 */
var _texpr = function(operator1, operator2, operand1, operand2, operand3) {
	this.operators = [operator1, operator2];

	this.operands = [
		autoParen(autoString(operand1)),
		autoParen(autoString(operand2)),
		autoParen(autoString(operand3))
	];

	/** @returns {!string} */
	this.toString = function() {
		return [
			this.operands[0],
			this.operators[0],
			this.operands[1],
			this.operators[1],
			this.operands[2]
		].join(' ');
	};
};

var _assign = function(lhand, rhand) {
	if (lhand == rhand) {
		return '';
	}

	if ((rhand instanceof _bexpr) && (autoString(lhand) == rhand.operands[0])) {
		return new _bexpr(rhand.operator + '=', lhand, rhand.operands[1]);
	}

	if (rhand instanceof Variable.functionPointer) {
		rhand = parenthesize(rhand);
	}

	return new _bexpr('=', lhand, rhand);
};

var _cast = function(source, type) {
	return new _uexpr(parenthesize(Global().printer.theme.types(type)) + ' ', source);
};

var _generic_call = function(function_name, args) {
	this.function_name = Extra.is.string(function_name) ? Cpp(Extra.replace.call(function_name)) : function_name;
	this.callargs = args || [];

	this.toString = function() {
		var fname = this.function_name;

		if (Extra.is.string(fname)) {
			fname = Global().printer.theme.callname(fname);
		}

		return [fname, parenthesize(this.callargs.join(', '))].join(' ');
	};
};

var _generic_method_call = function(object_this, function_name, args, method_sep, prefix, postfix) {
	this.object_this = Extra.is.string(object_this) ? Cpp(object_this) : object_this;
	this.function_name = Extra.is.string(function_name) ? Cpp(function_name) : function_name;
	this.callargs = args || [];
	this.method_sep = method_sep;
	this.prefix = prefix || "";
	this.postfix = postfix || "";

	this.toString = function() {
		var fname = this.function_name;

		if (Extra.is.string(fname)) {
			fname = Global().printer.theme.callname(fname);
		}

		return this.prefix + this.object_this + this.method_sep + [fname, parenthesize(this.callargs.join(', '))].join(' ') + this.postfix;
	};
};

var _generic_rotate = function(destination, source_a, rotation, bits, is_left) {
	this.call = 'rotate_' + (is_left ? 'left' : 'right') + bits;
	this.destination = destination;
	this.source_a = source_a;
	this.rotation = Extra.is.number(rotation) ? ("0x" + rotation.toString(16)) : rotation;

	this.toString = function() {
		var args = [autoString(this.source_a), autoString(this.rotation)];

		return [autoString(this.destination), '=',
			Global().printer.theme.callname(this.call),
			parenthesize(args.join(', '))
		].join(' ');
	};
};

var _generic_return = function(value) {
	this.value = value;

	this.toString = function(options) {
		var value = '';

		if (this.value) {
			value = ' ' + autoString(this.value);
		}

		return Global().printer.theme.flow('return') + value;
	};
};

var _generic_goto = function(label_or_address) {
	this.value = label_or_address;

	this.toString = function(options) {
		return [Global().printer.theme.flow('goto'), autoString(this.value)].join(' ');
	};
};

var _generic_flow = function(name) {
	this.name = name;

	this.toString = function(options) {
		return Global().printer.theme.flow(this.name);
	};
};

var _assign_object_field = function(destination, object, field, method_sep, is_from) {
	this.destination = destination;
	this.object = object;
	this.field = field;
	this.method_sep = method_sep;
	this.is_from = is_from;

	this.toString = function(options) {
		if (this.is_from) {
			return [
				this.object + this.method_sep + this.field,
				'=',
				this.destination
			].join(' ');
		}
		return [
			this.destination,
			'=',
			this.object + this.method_sep + this.field
		].join(' ');
	};
};

var _assign_array_at = function(destination, source, index, is_from) {
	this.destination = destination;
	this.source = source;
	this.index = index;
	this.is_from = is_from;

	this.toString = function(options) {
		if (this.is_from) {
			return [
				this.destination,
				'=',
				this.source,
				'[' + this.index + ']'
			].join(' ');
		}
		return [
			this.destination,
			'[' + this.index + ']',
			'=',
			this.source
		].join(' ');
	};
};

var _generic_throw = function(value) {
	this.value = value;

	this.toString = function(options) {
		var value = '';

		if (this.value) {
			value = ' ' + autoString(this.value);
		}

		return Global().printer.theme.flow('throw') + value;
	};
};

var _generic_objc_call = function(object_this, function_name, function_arguments) {
	this.object_this = object_this;
	this.function_name = function_name;
	this.callargs = function_arguments || [];

	this.toString = function() {
		var fname = this.object_this;
		if (Extra.is.string(object_this)) {
			fname = Global().printer.theme.callname(object_this);
		}

		return "[" + [fname, this.function_name].concat(this.callargs).join(' ') + "]";
	};
};

export default {
	/* COMMON */
	assign: _assign,
	cast: function(destination, source, type) {
		return _assign(destination, new _cast(source, type));
	},
	nop: function(asm) {
		return null;
	},
	/* JUMPS */
	goto: function(label_or_address) {
		return new _generic_goto(label_or_address);
	},
	call: function(function_name, function_arguments) {
		return new _generic_call(function_name, function_arguments);
	},
	return: function(value) {
		return new _generic_return(value);
	},
	break: function(value) {
		return new _generic_flow('break');
	},
	continue: function(value) {
		return new _generic_flow('continue');
	},
	/* BRANCHES */
	conditional_assign: function(destination, source_a, source_b, cond, src_true, src_false) {
		var condition = new Condition.convert(source_a, source_b, cond, false);

		return _assign(destination, new _texpr('?', ':', condition.toString(), src_true, src_false));
	},
	conditional_math: function(destination, source_a, source_b, cond, math_operand_a, math_operand_b, src_false, operation) {
		var condition = new Condition.convert(source_a, source_b, cond, false);
		var src_true = new _bexpr(operation, math_operand_a, math_operand_b);

		return _assign(destination, new _texpr('?', ':', condition.toString(), src_true, src_false));
	},
	/* MATH */
	increase: function(destination, source) {
		if (source == '1') {
			return new _uexpr_pf('++', destination);
		}

		return _assign(destination, new _bexpr('+', destination, source));
	},
	decrease: function(destination, source) {
		if (source == '1') {
			return new _uexpr_pf('--', destination);
		}

		return _assign(destination, new _bexpr('-', destination, source));
	},
	add: function(destination, source_a, source_b) {
		if ((destination == source_a) && (source_b == '1')) {
			return new _uexpr_pf('++', destination);
		}

		return _assign(destination, new _bexpr('+', source_a, source_b));
	},
	and: function(destination, source_a, source_b) {
		if (source_b == '0') {
			return _assign(destination, '0');
		}

		return _assign(destination, new _bexpr('&', source_a, source_b));
	},
	divide: function(destination, source_a, source_b) {
		return _assign(destination, new _bexpr('/', source_a, source_b));
	},
	module: function(destination, source_a, source_b) {
		return _assign(destination, new _bexpr('%', source_a, source_b));
	},
	multiply: function(destination, source_a, source_b) {
		return _assign(destination, new _bexpr('*', source_a, source_b));
	},
	negate: function(destination, source) {
		return _assign(destination, new _uexpr('-', source));
	},
	not: function(destination, source) {
		return _assign(destination, new _uexpr('~', source));
	},
	extend: function(destination, source, bits) {
		return _assign(destination, new _uexpr('(int' + bits + '_t) ', source));
	},
	subtract: function(destination, source_a, source_b) {
		if ((destination == source_a) && (source_b == '0')) {
			return null;
		} else if (destination == source_a && source_b == '1') {
			return new _uexpr_pf('--', destination);
		}

		return _assign(destination, new _bexpr('-', source_a, source_b));
	},
	or: function(destination, source_a, source_b) {
		if (source_b == '0') {
			return _assign(destination, source_a);
		}

		return _assign(destination, new _bexpr('|', source_a, source_b));
	},
	xor: function(destination, source_a, source_b) {
		if (source_a == source_b) {
			return _assign(destination, '0');
		}

		return _assign(destination, new _bexpr('^', source_a, source_b));
	},
	shift_left: function(destination, source_a, source_b) {
		return _assign(destination, new _bexpr('<<', source_a, source_b));
	},
	shift_right: function(destination, source_a, source_b) {
		return _assign(destination, new _bexpr('>>', source_a, source_b));
	},
	rotate_left: function(destination, source_a, source_b, bits) {
		Global().context.addDependency(new CCalls.rotate_left.fcn(bits));

		return new _generic_rotate(destination, source_a, source_b, bits, true);
	},
	rotate_right: function(destination, source_a, source_b, bits) {
		Global().context.addDependency(new CCalls.rotate_right.fcn(bits));

		return new _generic_rotate(destination, source_a, source_b, bits, false);
	},
	swap_endian: function(value, returns, bits) {
		Global().context.addDependency(new CCalls.swap_endian.fcn(bits));

		return _assign(returns, new _generic_call('SWAP' + bits, [value]));
	},
	swap: function(source_a, source_b, bits) {
		var t = Variable.uniqueName('tmp');
		var ops = [
			_assign(Extra.to.type(bits, false) + ' ' + t, source_a),
			_assign(source_a, source_b),
			_assign(source_b, t),
		];
		return new function(composed) {
			this.composed = composed;
		}(ops);
	},
	bit_mask: function(destination, source_a, source_b) {
		Global().context.addDependency(new CCalls.bit_mask.fcn());

		return _assign(destination, new _generic_call('BIT_MASK', [source_a, source_b]));
	},
	/* MEMORY */
	read_memory: function(pointer, register, bits, is_signed) {
		var value = (Extra.is.string(register) || Extra.is.number(register)) ? Variable.local(register.toString(), Extra.to.type(bits, is_signed)) : register;
		var ptr = (Extra.is.number(pointer) || Long.isLong(pointer)) ? Variable.pointer('0x' + pointer.toString(16), Extra.to.type(bits, is_signed)) : pointer;
		if (Extra.is.string(ptr)) {
			ptr = Variable.pointer(pointer, Extra.to.type(bits, is_signed));
		}
		return _assign(value, ptr);
	},
	write_memory: function(pointer, register, bits, is_signed) {
		var value = (Extra.is.string(register) || Extra.is.number(register)) ? Variable.local(register.toString(), Extra.to.type(bits, is_signed)) : register;
		var ptr = (Extra.is.number(pointer) || Long.isLong(pointer)) ? Variable.pointer('0x' + pointer.toString(16), Extra.to.type(bits, is_signed)) : pointer;
		if (Extra.is.string(ptr)) {
			ptr = Variable.pointer(pointer, Extra.to.type(bits, is_signed));
		}
		return _assign(ptr, value);
	},
	/* SPECIAL */
	composed: function(instructions) {
		return new function(composed) {
			this.composed = composed;
		}(instructions);
	},
	macro: function(macro, macro_rule) {
		Global().context.addMacro(macro_rule);

		return new function(macro) {
			this.macro = macro;

			this.toString = function() {
				return Global().printer.theme.macro(this.macro);
			};
		}(macro);
	},
	special: function(data) {
		return new function(data) {
			this.data = data;

			this.toString = function() {
				return Global().printer.auto(this.data);
			};
		}(data);
	},
	/* Object based langs */
	method_call: function(object_this, method_separator, function_name, function_arguments, prefix, postfix) {
		return new _generic_method_call(object_this, function_name, function_arguments, method_separator, prefix, postfix);
	},
	objc_call: function(object_this, function_name, function_arguments) {
		return new _generic_objc_call(object_this, function_name, function_arguments);
	},
	/* Object based langs */
	assign_to_object_field: function(destination, object, method_separator, field) {
		return new _assign_object_field(destination, object, field, method_separator, false);
	},
	assign_from_object_field: function(destination, object, method_separator, field) {
		return new _assign_object_field(destination, object, field, method_separator, true);
	},
	assign_to_array: function(destination, source, index) {
		return new _assign_array_at(destination, source, index, false);
	},
	assign_from_array: function(destination, source, index) {
		return new _assign_array_at(destination, source, index, true);
	},
	throw: function(value) {
		return new _generic_throw(value);
	},
	/* UNKNOWN */
	unknown: function(asm) {
		return new _generic_asm(asm);
	}
};
